Skip to main content
  1. « Go back to Articles

Reuse React prop types as transient props with Styled Components

Image of the PropsForStyling type code

When passing props to a styled component, the prop name should be prefixed with a dollar sign ($) in order to turn it into a transient prop. This prevents props meant to be consumed by styled components from being passed to the underlying React node or rendered to the DOM element, which may cause unintented effects or invalid HTML.

However, nesting styled components in other components frequently leads to repetition of the same prop names over and over again, with the only difference being the mentioned dollar sign. Consider the following example, where the isActive prop appears twice in TitleProps and CardProps. Managing more complex prop types can quickly become cumbersome.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import type { FunctionComponent } from 'react'
import styled from 'styled-components'

// Example adapted from https://styled-components.com/docs/api#using-custom-props

type TitleProps = {
  readonly $isActive?: boolean
}

const Title = styled.h1<TitleProps>`
  color: ${(props) =>
    props.$isActive ? props.theme.colors.main : props.theme.colors.secondary};
`

type CardProps = {
  isActive: boolean
}

export const Card: FunctionComponent<CardProps> = ({ isActive }) => {
  return (
    <>
      <Title $isActive={isActive}>Card title</Title>
      {/** .. other components .. */}
    </>
  )
}

We can remove this redundancy by implementing the PropsForStyling utility type, which re-maps all keys of T to be readonly, optional and start with the letter $, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/**
 * Re-maps all keys of `T` to be readonly, optional and start with the letter `$`.
 * This utility type converts React prop types to types for styled component props, where the `$` prevents the props from being passed to the DOM as HTML
 * attributes (transient props, see: https://styled-components.com/docs/api#transient-props).
 *
 * @example
 * ```ts
 * PropsForStyling<{ variant: 'S' | 'M' | 'L' }>
 * // ^^^ = { $variant?: 'S' | 'M' | 'L' }
 * ```
 */
export type PropsForStyling<T> = {
  readonly [P in keyof T as `$${string & P}`]?: T[P]
}

Technically, this type uses a combination of the following techniques (requiring TypeScript 4.1):

Using the PropsForStyling type, we can rewrite the example from above without repetition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import type { FunctionComponent } from 'react'
import styled from 'styled-components'

type CardProps = {
  isActive: boolean
}

// here we've replaced TitleProps with PropsForStyling<CardProps>
const Title = styled.h1<PropsForStyling<CardProps>>`
  color: ${(props) =>
    props.$isActive ? props.theme.colors.main : props.theme.colors.secondary};
`

export const Card: FunctionComponent<CardProps> = ({ isActive }) => {
  return (
    <>
      <Title $isActive={isActive}>Card title</Title>
      {/** .. other components .. */}
    </>
  )
}

Moreover, the types of all props of the parent component are now available on the nested styled component as well (you need to remember to pass their values, though). Arguably using this technique creates a very tight coupling between the components, which might not be desireable in all situations.

Dr. Ole Hüter
Author
Dr. Ole Hüter
Freelance Full Stack Web Developer